package com.sphericalchickens.opensesame;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.UnknownHostException;

import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import javax.net.SocketFactory;

public class Client extends Activity {
    private static final String PORT_KEY = "PORT_KEY";
    private static final String SERVER_KEY = "SERVER_KEY";

    public static final String WORK_TAG = "work";
    private Object mAddressLock = new Object();

    // @GuardedBy("mAddressLock")
    private EditText mServerEditText;

    private Object mResponsesLock = new Object();
    List<String> serverResponses = new ArrayList<String>(1000);

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // First time init, create the UI.
        if (savedInstanceState == null) {
            getFragmentManager().beginTransaction().add(android.R.id.content, new UiFragment(),
                    "UiFragment").commit();
        }
    }

    public static class UiFragment extends Fragment {
        RetainedFragment mWorkFragment;
        private SharedPreferences mPrefs;
        TextView mStatusMessage;
        Button mControlButton;

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            final View v = inflater.inflate(R.layout.client_main, container, false);

            // Watch for button clicks.
            v.findViewById(R.id.send_button).setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    // mWorkFragment.updateAddress();
                    EditText messageEditText = (EditText) v.findViewById(R.id.message_edit_text);
                    String message = messageEditText.getText().toString();
                    mWorkFragment.sendMessage(message + "\n");
                }
            });

            mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());

            int port = mPrefs.getInt(PORT_KEY, 2000);
            String server = mPrefs.getString(SERVER_KEY, null);
            EditText serverEditText = (EditText) v.findViewById(R.id.serverEditText);
            if (server != null) {
                serverEditText.setText(server + ":" + Integer.toString(port));
            }

            mStatusMessage = (TextView) v.findViewById(R.id.status_text);
            mControlButton = (Button) v.findViewById(R.id.control_button);

            mControlButton.setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View view) {
                    mWorkFragment.toggleDoor();
                }
            });

            serverEditText.setSelectAllOnFocus(true);
            return v;
        }

        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            FragmentManager fm = getFragmentManager();

            // Check to see if we have retained the worker fragment.
            mWorkFragment = (RetainedFragment) fm.findFragmentByTag(WORK_TAG);

            // If not retained (or first time running), we need to create it.
            if (mWorkFragment == null) {
                mWorkFragment = new RetainedFragment();
                // Tell it who it is working with.
                mWorkFragment.setTargetFragment(this, 0);
                fm.beginTransaction().add(mWorkFragment, WORK_TAG).commit();
            }
        }

        private void updateUi(RetainedFragment.DOOR_STATUS status) {
            switch (status) {
                case UNKNOWN:
                    mStatusMessage.setText(getString(R.string.garage_status, "unknown"));
                    mControlButton.setText("Get status");
                    break;
                case CLOSED:
                    mStatusMessage.setText(getString(R.string.garage_status, "closed"));
                    mControlButton.setText("Open door");
                    break;
                case OPEN:
                    mStatusMessage.setText(getString(R.string.garage_status, "open"));
                    mControlButton.setText("Close door");
                    break;
            }
        }
    }

    public static class RetainedFragment extends Fragment {
        private SharedPreferences mPrefs;

        private EditText mServerEditText;

        boolean mReady = false;
        boolean mQuiting = false;

        // State
        private final Object mLock = new Object();

        enum STATE {
            NOT_CONNECTED,
            CONNECTED
        }

        enum DOOR_STATUS {
            UNKNOWN,
            OPEN,
            CLOSED
        }

        private STATE mState = STATE.NOT_CONNECTED;
        private String mServer = "192.168.1.115";
        private int mPort = 2000;
        private Socket mSocket = null;
        private BufferedReader mReader = null;
        private BufferedWriter mWriter = null;
        private DOOR_STATUS mDoorStatus = DOOR_STATUS.UNKNOWN;

        Handler handler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                Bundle data = msg.getData();
                String message = data.getString("msg");

                if (message.compareToIgnoreCase("OPEN") == 0) {
                    mDoorStatus = DOOR_STATUS.OPEN;
                } else if (message.compareToIgnoreCase("CLOSED") == 0) {
                    mDoorStatus = DOOR_STATUS.CLOSED;
                }

                assert ((UiFragment) getTargetFragment()) != null;
                ((UiFragment) getTargetFragment()).updateUi(mDoorStatus);
            }
        };

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

            // Tell the framework to try to keep this fragment around
            // during a configuration change.
            setRetainInstance(true);

            mPrefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
        }

        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);

            mServerEditText =
                    (EditText) getTargetFragment().getView().findViewById(R.id.serverEditText);
            fetchDoorStatus();
        }

        @Override
        public void onDestroy() {
            closeSocket();
            super.onDestroy();
        }


        @Override
        public void onDetach() {
            // This fragment is being detached from its activity.  We need
            // to make sure its thread is not going to touch any activity
            // mState after returning from this function.
            super.onDetach();
        }

        private void updateAddress() {
            String address = mServerEditText.getText().toString();
            String parts[] = address.split(":");
            boolean changed = false;
            synchronized (mLock) {
                if (parts.length > 0) {
                    if (!parts[0].equals(mServer)) {
                        mServer = parts[0];
                        changed = true;
                    }
                }

                if (parts.length > 1) {
                    int newPort = Integer.parseInt(parts[1]);
                    if (newPort != mPort) {
                        mPort = newPort;
                        changed = true;
                    }
                }
            }

            if (changed) {
                mPrefs.edit().putString(SERVER_KEY, mServer).putInt(PORT_KEY, mPort).apply();
                closeSocket();
            }
        }

        DOOR_STATUS getStatus() {
            if (mDoorStatus == DOOR_STATUS.UNKNOWN) {
                fetchDoorStatus();
            }
            return mDoorStatus;
        }

        void fetchDoorStatus() {
            sendMessage("\nstatus\n");
        }

        public void toggleDoor() {
            switch (mDoorStatus) {
                case UNKNOWN:
                    fetchDoorStatus();
                    break;
                case OPEN:
                    closeDoor();
                    break;
                case CLOSED:
                    openDoor();
                    break;
            }
        }

        private void openDoor() {
            sendMessage("open\n");
        }

        private void closeDoor() {
            sendMessage("close\n");
        }

        public void sendMessage(String message) {
            ClientSendTask clientSendTask = new ClientSendTask();
            clientSendTask.execute(message);
        }

        private void closeSocket() {
            synchronized (mLock) {
                if (mSocket != null) {
                    try {
                        // TODO(dkhawk) kill the reader thread first?
                        mSocket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        class ClientSendTask extends AsyncTask<String, Integer, String> {
            @Override
            protected String doInBackground(String... strings) {
                String message = strings[0];

                synchronized (mLock) {
                    try {
                        updateAddress();

                        if (mState == STATE.NOT_CONNECTED) {
                            openSocket();
                        }

                        mWriter.write(message);
                        mWriter.flush();

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }

                return null;
            }
        }

        private class ClientException extends Exception {
            public ClientException(Exception e) {
                super(e);
            }
        }

        private void openSocket() throws ClientException {
            try {
                mSocket = SocketFactory.getDefault().createSocket(mServer, mPort);
            } catch (UnknownHostException e) {
                e.printStackTrace();
                Toast.makeText(getActivity(), "Unknown Host Exception", Toast.LENGTH_SHORT).show();
                throw new ClientException(e);
            } catch (IOException e) {
                e.printStackTrace();
                Toast.makeText(getActivity(), "Other Exception", Toast.LENGTH_SHORT).show();
                throw new ClientException(e);
            }
            initDataManagers();
        }

        protected void initDataManagers() throws ClientException {
            try {
                mReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream(), "UTF-8"));
                mWriter = new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream(), "UTF-8"));

                mState = STATE.CONNECTED;

                Thread readerThread = new Thread(new Runnable() {

                    @Override
                    public void run() {
                        try {
                            while (true) {
                                String line = mReader.readLine();
                                Log.v("Client", line);

                                // Strip "hello" message
                                line = line.replace("*HELLO*", "");

                                Message msg = handler.obtainMessage();
                                Bundle bundle = new Bundle();
                                bundle.putString("msg", line);
                                msg.setData(bundle);
                                handler.sendMessage(msg);
                                // TODO(dkhawk): add server lines.  should work with observer?
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                            try {
                                mSocket.close();
                            } catch (IOException e1) {
                                e1.printStackTrace();
                            }
                        }
                    }
                });

                readerThread.start();

            } catch (UnsupportedEncodingException e) {
                throw new ClientException(e);
            } catch (IOException e) {
                throw new ClientException(e);
            }
        }

    }
}
